package fr.upmc.alasca.centreCalcul.interne.controleurAdmission;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.TimeUnit;

import fr.upmc.alasca.centreCalcul.interne.controleurAdmission.interfaces.ControleurAdmissionClientI;
import fr.upmc.alasca.centreCalcul.interne.controleurAdmission.interfaces.ControleurAdmissionI;
import fr.upmc.alasca.centreCalcul.interne.controleurAdmission.ports.ControleurAdmissionClientInboundPort;
import fr.upmc.alasca.centreCalcul.interne.controleurAdmission.ports.ControleurAdmissionInboundPort;
import fr.upmc.alasca.centreCalcul.interne.controleurApplication.ControleurApplication;
import fr.upmc.alasca.centreCalcul.interne.controleurApplication.connectors.ControleurApplicationCapteurTEConnector;
import fr.upmc.alasca.centreCalcul.interne.controleurApplication.interfaces.ControleurApplicationCapteurTEI;
import fr.upmc.alasca.centreCalcul.interne.controleurApplication.ports.ControleurApplicationCapteurTEOutboundPort;
import fr.upmc.alasca.centreCalcul.interne.machinePhysique.connectors.MachinePhysiqueAdmissionConnector;
import fr.upmc.alasca.centreCalcul.interne.machinePhysique.interfaces.MachinePhysiqueAdmissionI;
import fr.upmc.alasca.centreCalcul.interne.machinePhysique.ports.MachinePhysiqueAdmissionOutboundPort;
import fr.upmc.alasca.centreCalcul.interne.repartiteurRequetes.RepartiteurRequetes;
import fr.upmc.alasca.centreCalcul.objets.Delais;
import fr.upmc.alasca.centreCalcul.objets.DemandeAdmission;
import fr.upmc.alasca.centreCalcul.objets.DemandeChangementCoeur;
import fr.upmc.alasca.centreCalcul.outils.LogMessage;
import fr.upmc.components.AbstractComponent;
import fr.upmc.components.cvm.AbstractCVM;
import fr.upmc.components.cvm.AbstractDistributedCVM;
import fr.upmc.components.cvm.pre.dcc.DynamicComponentCreationConnector;
import fr.upmc.components.cvm.pre.dcc.DynamicComponentCreationI;
import fr.upmc.components.cvm.pre.dcc.DynamicComponentCreationOutboundPort;


/**
 * Composant qui reçoit des demandes d'exécution d'application.
 * Il connait la topographie du centre de calcul (les machines physiques qui le
 * composent) ainsi que les répartiteur de requêtes créés.
 * @author Antoine Lavaire, Sabri Labassi, Thibaut Fleury, Ryme Fantazi
 */
public	class	ControleurAdmission
extends			AbstractComponent
implements		ControleurAdmissionI, ControleurAdmissionClientI
{
	/* ------------------------------------------------------------- ATTRIBUT */
	protected	String													portUri;
	protected	Boolean													estDistribue;
	
	// Compteur de demandes d'exécution d'application
	protected	int														nbDemandesRecues;
	// Liste des URI des machines physiques du centre de calcul
	protected	List<String>											machinesPhysiquesUri;
	// Associations (URI de la machine physique -> port d'utilisation de la machine physique)
	protected	Map<String, MachinePhysiqueAdmissionOutboundPort>		machinesPhysiques;
	
	// Associations (ID de l'application -> port d'utilisation du controleur d'application)
	protected	Map<String, ControleurApplicationCapteurTEOutboundPort>	controleursApplications;

	// Demande de changement de coeur en attente d'arbitrage
	protected	Map<String, DemandeChangementCoeur>						demandesChangementCoeur;
	// Temps d'attente des requêtes en ms
	protected	int														tempsAttenteDemandes;

	protected	Float 													frequenceMax;
	protected	Float 													frequenceMin;




	/* --------------------------------------------------------- CONSTRUCTEUR */
	public		ControleurAdmission
	(
		String			p_portUri,
		boolean			p_estDistribue,
		List<String>	p_machinesPhysiquesUri,
		float			p_frequenceMin,
		float			p_frequenceMax
	) throws	Exception
	{
		super(true, true);
		
		LogMessage.printMessage("Admission", null,
				"Création du contrôleur d'admission.");
		
		portUri = p_portUri;
		estDistribue = p_estDistribue;
		
		nbDemandesRecues = 0;
		machinesPhysiquesUri = p_machinesPhysiquesUri;
		
		demandesChangementCoeur = new HashMap<String, DemandeChangementCoeur>();
		tempsAttenteDemandes = 10000;

		frequenceMax = p_frequenceMax;
		frequenceMin = p_frequenceMin;
		
		// Ajout de l'interface offerte au générateur de demandes.
		this.addOfferedInterface(ControleurAdmissionClientI.class);
		ControleurAdmissionClientInboundPort pca =
				new ControleurAdmissionClientInboundPort(portUri + "_Client", this);
		this.addPort(pca);
		if(estDistribue)
			pca.publishPort();
		else
			pca.localPublishPort();
		
		// Ajout de l'interface offerte au contrôleur d'application.
		this.addOfferedInterface(ControleurAdmissionI.class);
		ControleurAdmissionInboundPort pc =
				new ControleurAdmissionInboundPort(portUri + "_", this);
		this.addPort(pc);
		if(estDistribue)
			pc.publishPort();
		else
			pc.localPublishPort();
		
		
		// Ajout de l'interface requise pour l'utilisation des machines physiques.
		this.addRequiredInterface(MachinePhysiqueAdmissionI.class);
		machinesPhysiques = new HashMap<String, MachinePhysiqueAdmissionOutboundPort>();
		
		// Ajout de l'interface requise pour la création dynamique de composant.
		this.addRequiredInterface(DynamicComponentCreationI.class);
		
		// Ajout de l'interface requise pour l'utilisation du controleur d'application.
		this.addRequiredInterface(ControleurApplicationCapteurTEI.class);
	}
	



	/* -------------------------------------------------------------- METHODE */
	public	void									deploy
	() throws	Exception
	{
		synchronized (machinesPhysiques) {
			// Pour chaque machine physique du centre de calcul...
			for(String uri : machinesPhysiquesUri) {
				// Création du port d'utilisation de la machine physique.
				MachinePhysiqueAdmissionOutboundPort pmp = new MachinePhysiqueAdmissionOutboundPort(uri + "_Outbound", this);
				this.addPort(pmp);
				pmp.localPublishPort();
				pmp.doConnection(uri + "_Admission", MachinePhysiqueAdmissionConnector.class.getName());
				
				assert pmp != null && pmp instanceof MachinePhysiqueAdmissionI;
				
				machinesPhysiques.put(uri, pmp);
			}
		}
	}
	
	
	/* ControleurAdmissionI ------------------------------------------------- */
	@Override
	public	void									ajouterCoeurs
	(
		DemandeChangementCoeur	p_demandeChangementCoeur
	) throws	Exception
	{
		LogMessage.printMessage("Admission", null,
				"A reçu une demande d'ajout de coeur pour l'application " + p_demandeChangementCoeur.getUriApplication() + ".");
		
		synchronized(demandesChangementCoeur) {
			demandesChangementCoeur.put(
					p_demandeChangementCoeur.getUriApplication(),
					p_demandeChangementCoeur);
		}
		
		this.scheduleTask(
				new ComponentTask() {
					@Override
					public void run() {
						try {
							arbitrage();
						}
						catch(Exception e) {
							e.printStackTrace();
						}
					}
				},
				tempsAttenteDemandes, TimeUnit.MILLISECONDS);
	}


	/**
	 * L'arbitrage va essayer de répondre à chaque demande d'ajout de coeur
	 * par ordre de priorité.
	 * Les applications les plus en retard sont traitées en premier.
	 * @throws Exception
	 */
	private	void									arbitrage
	() throws	Exception
	{
		LogMessage.printMessage("Admission", null,
				"Arbitrage en cours (" + demandesChangementCoeur.size() + " demande(s) à traiter).");
		
		synchronized(demandesChangementCoeur) {
			// Tant qu'il y a des demandes à traiter...
			while(!demandesChangementCoeur.isEmpty()) {
				DemandeChangementCoeur plusUrgente = null;
				
				for(DemandeChangementCoeur dcc : demandesChangementCoeur.values()) {
					// Si la demande en cours est plus urgente
					if(plusUrgente == null || plusUrgente.getEcartPerformancesSec() < dcc.getEcartPerformancesSec())
						plusUrgente = dcc;
				}
				
				demandesChangementCoeur.remove(plusUrgente.getUriApplication());
				
				List<String> p_uriMachinesPhysiques = ajouterCoeurs(
						plusUrgente.getUriApplication(),
						plusUrgente.getNbCoeursAReserver());
				
				ControleurApplicationCapteurTEOutboundPort cacteo =
						new ControleurApplicationCapteurTEOutboundPort(
								plusUrgente.getUriApplication() + "_ControleurApplicationOutbound",
								this);
				this.addPort(cacteo);

				if(estDistribue)
					cacteo.publishPort();
				else
					cacteo.localPublishPort();
				
				cacteo.doConnection(
						plusUrgente.getUriApplication() + "_ControleurApplication_PushTE",
						ControleurApplicationCapteurTEConnector.class.getName());
				
				LogMessage.printMessage("Admission", null,
						"Demande d'ajout de coeurs traitée pour l'application " + plusUrgente.getUriApplication() + ".");
				
				cacteo.callbackAjouterCoeurs(p_uriMachinesPhysiques);
				
				cacteo.doDisconnection();
			}
		}
	}


	/**
	 * Tente d'ajouter des coeurs à une application.
	 * @param p_uriApplication L'URI de l'application qui veut des coeurs
	 * @param p_nbCoeursAAjouter Le nombre de coeurs voulus pour l'application
	 * @return La liste des URI des machines physiques sur lesquelles des coeurs
	 * ont pu être ajoutés
	 * @throws Exception
	 */
	private	List<String>							ajouterCoeurs
	(
		String	p_uriApplication,
		int		p_nbCoeursAAjouter
	) throws	Exception
	{
		assert p_nbCoeursAAjouter > 0;
		
		List<String> reponse = new ArrayList<String>();
		synchronized(machinesPhysiques) {
			for(Entry<String, MachinePhysiqueAdmissionOutboundPort> mp : machinesPhysiques.entrySet()) {
				// Réservation des coeurs pour l'application.
				int nbCoeursFaits = mp.getValue().reserverCoeur(p_uriApplication, p_nbCoeursAAjouter);
				// Si on a réussi à allouer des coeurs...
				if(nbCoeursFaits > 0) {
					reponse.add(mp.getKey());
					p_nbCoeursAAjouter -= nbCoeursFaits;
				}
				
				// Si on a ajouté assez de coeurs...
				if(p_nbCoeursAAjouter == 0)
					break;
			}
		}
		
		return reponse;
	}
	/* ControleurAdmissionI ------------------------------------------------- */


	/* ControleurAdmissionClientI ------------------------------------------- */
	@Override
	public	String									soumettreApplication
	(
		DemandeAdmission	p_demandeAdmission
	) throws	Exception
	{
		LogMessage.printMessage("Admission", null,
				"Nouvelle application soumise " + p_demandeAdmission.getNomApplication() + ".");
		
		String uriApplication = p_demandeAdmission.getNomApplication() + "_" + (nbDemandesRecues++);
		//TODO déterminer le nombre de coeurs à partir des autres paramètres de la demande
		int nbCoeursAReserver = p_demandeAdmission.getNbCoeursVoulus();
		ArrayList<String> machinesUtilisees = new ArrayList<String>();
		
		synchronized(machinesPhysiques) {
			for(Entry<String, MachinePhysiqueAdmissionOutboundPort> mp : machinesPhysiques.entrySet()) {
				// Réservation des coeurs pour l'application.
				int nbCoeursFaits = mp.getValue().reserverCoeur(uriApplication, nbCoeursAReserver);
				
				LogMessage.printMessage("Admission", null,
						"L'application " + p_demandeAdmission.getNomApplication()
							+ " a réservé " + nbCoeursFaits + " coeurs sur la machine " + mp.getKey() + ".");
				
				// Si on a réussi à réserver des coeurs...
				if(nbCoeursFaits > 0) {
					machinesUtilisees.add(mp.getKey());
					nbCoeursAReserver -= nbCoeursFaits;
				}
				
				// Si on a réservé assez de coeurs...
				if(nbCoeursAReserver == 0)
					break;
			}
		}
		
		// Si on a réussi à allouer assez de ressources pour l'application...
		if(nbCoeursAReserver == 0) {
			LogMessage.printMessage("Admission", null,
					"L'application " + p_demandeAdmission.getNomApplication() 
					+ " a pu réserver toutes ses ressources désirées et va donc être déployée.");
			
			// Creation du controleur d'application.
			creerComposant(
				ControleurApplication.class, 
				new Object[] {
					uriApplication, 
					estDistribue,
					p_demandeAdmission.getDureeValiditeRequeteMS(), 
					p_demandeAdmission.getTempsReponseMax(), 
					p_demandeAdmission.getTempsReponseMin(),
					portUri,
					machinesUtilisees,
					frequenceMin,
					frequenceMax
				},
				AbstractDistributedCVM.thisJVMURI // FIXME local
			);
			
			// Creation du repartiteur de requetes.
			creerComposant(
				RepartiteurRequetes.class,
				new Object[] {
					uriApplication,
					estDistribue,
					uriApplication + "_ControleurApplication",
					new Delais(5, TimeUnit.SECONDS)
				},
				AbstractDistributedCVM.thisJVMURI // FIXME local
			);
			
			// Renvoi de l'identifiant de l'application.
			return uriApplication;
		}
		// Si on n'a pas réussi à allouer assez de ressources à l'application...
		else {
			LogMessage.printMessage("Admission", null,
					"L'application " + p_demandeAdmission.getNomApplication() 
						+ " n'a pas pu être déployée et va donc relacher toutes ses ressources réservées.");
			
			synchronized(machinesPhysiques) {
				// On libère les coeurs réservés inutilement.
				for(String uri : machinesUtilisees) {
					MachinePhysiqueAdmissionOutboundPort mpa = machinesPhysiques.get(uri);
					mpa.relacherCoeur(uriApplication, null);
				}
			}
			
			return null;
		}
	}


	@Override
	public	void									arreterApplication
	(
		String	p_uriApplication
	) throws	Exception
	{
		LogMessage.printMessage("Admission", null,
				"Demande d'arrêt de l'application " + p_uriApplication + ".");
		
		synchronized(demandesChangementCoeur) {
			demandesChangementCoeur.remove(p_uriApplication);
		}
		
		// On se connecte au controleur d'application pour lui demander
		// de relâcher tous ses coeurs et de désallouer les MV.
		ControleurApplicationCapteurTEOutboundPort cacteo =
				new ControleurApplicationCapteurTEOutboundPort(
						p_uriApplication + "_ControleurApplicationOutbound", this);
		this.addPort(cacteo);

		if(estDistribue)
			cacteo.publishPort();
		else
			cacteo.localPublishPort();
		
		try {
			cacteo.doConnection(
					p_uriApplication + "_ControleurApplication_PushTE",
					ControleurApplicationCapteurTEConnector.class.getName());
			cacteo.arreterApplication();
		}
		catch(Exception e) {
			LogMessage.printMessage("Admission", null,
					"L'application " + p_uriApplication + " est déjà arrêtée.");
		}
	}
	/* ControleurAdmissionClientI ------------------------------------------- */
	
	
	/**
	 * Permet de créer un composant sur une JVM.
	 * @param p_class La classe du composant à créer.
	 * @param p_params Les paramêtres du constructeur du composant.
	 * @param p_uriJVM L'URI de la JVM sur laquelle instancier le composant.
	 * @throws Exception
	 */
	protected	void								creerComposant
	(
		Class<?>	p_class,
		Object[]	p_params,
		String		p_uriJVM
	) throws	Exception
	{
		assert p_class != null;
		assert p_params != null;
		assert p_uriJVM != null;
		
		DynamicComponentCreationOutboundPort pdcc = connexionDCC(p_uriJVM);

		pdcc.createComponent(p_class.getName(), p_params);

		deconnexionDCC(pdcc);
	}
	
	
	/**
	 * Etablit la connexion avec le serveur de création de composant.
	 * @param p_uriServeur L'URI du composant de création.
	 * @return Le port d'utilisation du créateur de composant.
	 * @throws Exception
	 */
	private DynamicComponentCreationOutboundPort 	connexionDCC
	(
		String	p_uriServeur
	) throws Exception
	{
		DynamicComponentCreationOutboundPort pdcc = new DynamicComponentCreationOutboundPort(this);
		pdcc.localPublishPort();
	
		this.addPort(pdcc);
		
		if(estDistribue)
			pdcc.doConnection(
					this.portUri,
					DynamicComponentCreationConnector.class.getCanonicalName());
		else
			pdcc.doConnection(
					AbstractCVM.DYNAMIC_COMPONENT_CREATOR_INBOUNDPORT_URI,
					DynamicComponentCreationConnector.class.getCanonicalName());

		return pdcc;
	}
	
	
	/**
	 * Déconnecte un port.
	 * @param p_pdcc Le port à déconnecter.
	 * @throws Exception
	 */
	private void 									deconnexionDCC
	(
		DynamicComponentCreationOutboundPort	p_pdcc
	) throws	Exception
	{
		p_pdcc.doDisconnection();
		p_pdcc.destroyPort();
	}
}
